/*
 * Copyright 2018 requery.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.requery.android.sqlite;

import android.annotation.SuppressLint;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

/**
 * {@link PreparedStatement} implementation using Android's local SQLite database.
 */
@SuppressLint("SimpleDateFormat")
public abstract class BasePreparedStatement extends BaseStatement implements PreparedStatement {

    private static final char[] hex = "0123456789ABCDEF".toCharArray();

    static final ThreadLocal<DateFormat> ISO8601_FORMAT = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            format.setTimeZone(TimeZone.getTimeZone("UTC"));
            return format;
        }
    };

    private String sql;
    protected final int autoGeneratedKeys;
    protected List<Object> bindings;
    private Map<Integer, byte[]> blobLiterals;

    protected BasePreparedStatement(BaseConnection connection, String sql, int autoGeneratedKeys)
        throws SQLException {
        super(connection);
        if (sql == null) {
            throw new SQLException("null sql");
        }
        if (autoGeneratedKeys != RETURN_GENERATED_KEYS) {
            bindings = new ArrayList<>(4);
        }
        this.sql = sql;
        this.autoGeneratedKeys = autoGeneratedKeys;
    }

    protected abstract void bindNullOrString(int index, Object value);

    protected abstract void bindLong(int index, long value);

    protected abstract void bindDouble(int index, double value);

    protected abstract void bindBlob(int index, byte[] value);

    protected String[] bindingsToArray() {
        String[] args = new String[bindings.size()];
        for (int i = 0; i < bindings.size() ; i++) {
            Object value = bindings.get(i);
            if (value != null) {
                args[i] = value.toString();
            }
        }
        return args;
    }

    private static String byteToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(hex[(b >> 4) & 0xF]);
            sb.append(hex[(b & 0xF)]);
        }
        return sb.toString();
    }

    // inlines a blob literal into the sql statement since it can't be used as bind parameter
    protected void bindBlobLiteral(int index, byte[] value) {
        if (blobLiterals == null) {
            blobLiterals = new LinkedHashMap<>();
        }
        blobLiterals.put(index, value);
    }

    private String inlineBlobLiterals() {
        StringBuilder sb = new StringBuilder();
        String[] parts = sql.split("\\?");
        for (int i = 0; i < parts.length; i++) {
            sb.append(parts[i]);
            if (blobLiterals.containsKey(i + 1)) {
                sb.append("x'").append(byteToHexString(blobLiterals.get(i + 1))).append("'");
            } else if (i < parts.length - 1) {
                sb.append("?");
            }
        }
        return sb.toString();
    }

    protected String getSql() {
        if (blobLiterals != null && !blobLiterals.values().isEmpty()) {
            return inlineBlobLiterals();
        }
        return sql;
    }

    @Override
    public void addBatch() throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public ResultSetMetaData getMetaData() {
        return null;
    }

    @Override
    public ParameterMetaData getParameterMetaData() {
        return null;
    }

    @Override
    public void setArray(int parameterIndex, Array x) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x, int length)
            throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setBigDecimal(int parameterIndex, BigDecimal x) {
        bindNullOrString(parameterIndex, x);
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setBlob(int parameterIndex, Blob x) throws SQLException {
        setBytes(parameterIndex, x.getBytes(0, (int) x.length()));
    }

    @Override
    public void setBoolean(int parameterIndex, boolean x) {
        long value = x ? 1 : 0;
        bindLong(parameterIndex, value);
    }

    @Override
    public void setByte(int parameterIndex, byte x) {
        bindLong(parameterIndex, (long) x);
    }

    @Override
    public void setBytes(int parameterIndex, byte[] x) {
        bindBlob(parameterIndex, x);
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setClob(int parameterIndex, Clob x) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setDate(int parameterIndex, Date x) {
        setDate(parameterIndex, x, null);
    }

    @Override
    public void setDate(int parameterIndex, Date x, Calendar cal) {
        bindNullOrString(parameterIndex, x == null ? null : ISO8601_FORMAT.get().format(x));
    }

    @Override
    public void setDouble(int parameterIndex, double x) {
        bindDouble(parameterIndex, x);
    }

    @Override
    public void setFloat(int parameterIndex, float x) {
        bindDouble(parameterIndex, (double) x);
    }

    @Override
    public void setInt(int parameterIndex, int x) {
        bindLong(parameterIndex, (long) x);
    }

    @Override
    public void setLong(int parameterIndex, long x) {
        bindLong(parameterIndex, x);
    }

    @Override
    public void setNull(int parameterIndex, int sqlType) {
        bindNullOrString(parameterIndex, null);
    }

    @Override
    public void setNull(int paramIndex, int sqlType, String typeName) {
        bindNullOrString(paramIndex, null);
    }

    @Override
    public void setObject(int parameterIndex, Object x) throws SQLException {
        if (x == null) {
            setNull(parameterIndex, Types.NULL);
        } else {
            if (x instanceof String) {
                setString(parameterIndex, x.toString());
            } else if (x instanceof Byte) {
                setByte(parameterIndex, (Byte) x);
            } else if (x instanceof Short) {
                setShort(parameterIndex, (Short) x);
            } else if (x instanceof Integer) {
                setInt(parameterIndex, (Integer) x);
            } else if (x instanceof Long) {
                setLong(parameterIndex, (Long) x);
            } else if (x instanceof Double) {
                setDouble(parameterIndex, (Double) x);
            } else if (x instanceof Float) {
                setFloat(parameterIndex, (Float) x);
            } else if (x instanceof Boolean) {
                setLong(parameterIndex, (Boolean) x ? 1 : 0);
            } else if (x instanceof byte[]) {
                setBytes(parameterIndex, (byte[]) x);
            } else if (x instanceof Date) {
                setDate(parameterIndex, (Date) x);
            } else if (x instanceof java.util.Date) {
                java.util.Date date = (java.util.Date) x;
                setDate(parameterIndex, new Date(date.getTime()));
            } else if (x instanceof BigDecimal) {
                setBigDecimal(parameterIndex, (BigDecimal) x);
            } else {
                throw new SQLException("unhandled type " + x.getClass().getCanonicalName());
            }
        }
    }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType)
            throws SQLException {
        if (x == null || targetSqlType == Types.NULL) {
            setNull(parameterIndex, Types.NULL);
            return;
        }
        switch (targetSqlType) {
            case Types.INTEGER:
            case Types.SMALLINT:
                if (x instanceof Integer) {
                    bindLong(parameterIndex, ((Integer) x).longValue());
                } else if (x instanceof Long) {
                    bindLong(parameterIndex, (Long) x);
                } else if (x instanceof Short) {
                    bindLong(parameterIndex, ((Short) x).longValue());
                }
                break;
            case Types.TINYINT:
                if (x instanceof Byte) {
                    bindLong(parameterIndex, ((Byte) x).longValue());
                }
                break;
            case Types.DOUBLE:
            case Types.FLOAT:
            case Types.REAL:
                if (x instanceof Double) {
                    setDouble(parameterIndex, (Double) x);
                } else if (x instanceof Float) {
                    setFloat(parameterIndex, (Float) x);
                }
                break;
            case Types.DECIMAL:
                if (x instanceof BigDecimal) {
                    setBigDecimal(parameterIndex, (BigDecimal) x);
                }
                break;
            case Types.BLOB:
            case Types.BINARY:
            case Types.VARBINARY:
                setBytes(parameterIndex, (byte[]) x);
                break;
            case Types.BOOLEAN:
                Boolean value = (Boolean) x;
                setBoolean(parameterIndex, value);
                break;
            case Types.VARCHAR:
            case Types.NVARCHAR:
                String string = x instanceof String ?
                        (String) x : x.toString();
                setString(parameterIndex, string);
                break;
            case Types.DATE:
                if (x instanceof Date) {
                    Date date = (Date) x;
                    setLong(parameterIndex, date.getTime());
                } else if (x instanceof java.util.Date) {
                    java.util.Date date = (java.util.Date) x;
                    setLong(parameterIndex, date.getTime());
                }
                break;
            case Types.TIMESTAMP:
                if (x instanceof Timestamp) {
                    Timestamp timestamp = (Timestamp) x;
                    setLong(parameterIndex, timestamp.getTime());
                }
                break;
            case Types.BIGINT:
                if (x instanceof BigInteger) {
                    BigInteger bigInteger = (BigInteger) x;
                    bindNullOrString(parameterIndex, bigInteger.toString());
                }
                break;
            default:
                throw new SQLException("unhandled type " + targetSqlType);
        }
    }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType, int scale)
            throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setRef(int parameterIndex, Ref x) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setShort(int parameterIndex, short x) {
        bindLong(parameterIndex, (long) x);
    }

    @Override
    public void setString(int parameterIndex, String x) {
        bindNullOrString(parameterIndex, x);
    }

    @Override
    public void setTime(int parameterIndex, Time x) {
        if (x == null) {
            bindNullOrString(parameterIndex, null);
        } else {
            bindLong(parameterIndex, x.getTime());
        }
    }

    @Override
    public void setTime(int parameterIndex, Time x, Calendar cal) {
        if (x == null) {
            bindNullOrString(parameterIndex, null);
        } else {
            bindLong(parameterIndex, x.getTime());
        }
    }

    @Override
    public void setTimestamp(int parameterIndex, Timestamp x) {
        if (x == null) {
            bindNullOrString(parameterIndex, null);
        } else {
            bindLong(parameterIndex, x.getTime());
        }
    }

    @Override
    public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) {
        if (x == null) {
            bindNullOrString(parameterIndex, null);
        } else {
            bindLong(parameterIndex, x.getTime());
        }
    }

    @Override
    public void setUnicodeStream(int parameterIndex, InputStream x, int length)
            throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setURL(int parameterIndex, URL x) {
        bindNullOrString(parameterIndex, x);
    }

    @Override
    public void setRowId(int parameterIndex, RowId x) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setNString(int parameterIndex, String x) {
        bindNullOrString(parameterIndex, x);
    }

    @Override
    public void setNCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setNClob(int parameterIndex, NClob value) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream inputStream, long length)
            throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream inputStream, long length)
            throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream inputStream) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream inputStream) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setNCharacterStream(int parameterIndex, Reader reader) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setClob(int parameterIndex, Reader reader) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void setNClob(int parameterIndex, Reader reader) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public String toString() {
        return sql;
    }
}
